[小ネタ] Node.jsのストリーム書き込みでSystemErrorが出た場合の対処法
はじめに
Node.jsのストリーム書き込み使いますか?私は最近Glueジョブの検証で、Firehoseから出力された大量テキストデータを用意する必要があり、使いました。Firehoseを経由して作っても良いのですが、お金がかかるので、ローカルで1つのテキストファイルを用意して、split
とgzip
コマンドで用意しました。
このローカルで1つのテキストファイルを作る際に、以下のエラーが出ました。対処法が検索しても見つからなかったの書くことにしました。
SystemError [ERR_SYSTEM_ERROR]: A system error occurred: undefined returned undefined (undefined)
本エラーは、Node.jsでシステムレベルでエラーが発生しており、具体的な原因はエラーメッセージがないため推測に基づく点とエラーの内容はコードを実行するPCのスペックに大きく左右される点ご容赦頂けたらと思います。
環境
要素 | 内容 | 補足 |
---|---|---|
Node.js | v18.18.2 | 20系だとts-nodeでESM形式がうまく動作しなかったため |
その他気になる点は、ソースコードを参照ください。
コード
import * as fs from 'fs'; import { DateTime } from 'luxon'; const DIST_PATH = 'dist'; const CONTENT_LENGTH = 500; const CONTENT = `${'a'.repeat(CONTENT_LENGTH)}\n}`; const WRITE_NUM = 5_000_000; const main = async () => { if (!fs.existsSync(DIST_PATH)) { fs.mkdirSync(DIST_PATH); } const now = DateTime.now(); const exportPath = `${DIST_PATH}/${now.toFormat('yyyyMMdd-HHmm')}.txt`; const ws = fs.createWriteStream(exportPath); for (let i = 0; i < WRITE_NUM; i++) { ws.write(CONTENT); drawString(`${i}/${WRITE_NUM}`); } ws.end(); }; const clearCurrentLine = (): void => { process.stdout.write('\r\x1b[2K'); }; const drawString = (str: string): void => { clearCurrentLine(); process.stdout.write(str); }; await main();
npx ts-node ./src/index.mts 4999999/5000000SystemError [ERR_SYSTEM_ERROR]: A system error occurred: undefined returned undefined (undefined) at new SystemError (node:internal/errors:256:5) at new NodeError (node:internal/errors:367:7) at node:internal/fs/streams:446:10 at FSReqCallback.wrapper [as oncomplete] (node:fs:955:5) { code: 'ERR_SYSTEM_ERROR', info: 'writev failed', errno: [Getter/Setter], syscall: [Getter/Setter] }
- ws.write(CONTENT); + if (!ws.write(CONTENT)) { + await new Promise((resolve) => ws.once('drain', resolve)); + } drawString(`${i}/${WRITE_NUM}`);
解説
バックプレッシャー
ストリーム処理において、データの生産側(今回だとサンプルデータの生成)とデータの消費側(今回だとファイルの書き込み)で、生産者が消費者よりも速くデータを生成する状況では、消費者が受け取ったデータを処理しきれずにバッファが溢れる恐れがあります。
データの生成者(プロデューサー)と消費者(コンシューマー)の間でデータの流れを調整する機構をバックプレッシャーと呼びます。
Node.jsのストリームとバックプレッシャー
Node.jsはストリームが自動的にバックプレッシャーを管理しています。ws.write
メソッドでfalse
が返却された場合、バッファが一杯であることを示しています。正常に動作するコードは、その間はデータの生産を止めます。消費側が再びデータの書き込みを受け入れる準備ができた時点でdrain
イベントが発生します。このイベントが発生したら書き込みを再開するという形です。
システムエラーの原因は、エラーが起きたコードと正常に動作するコードを比較すると、消費者側が受け取ったデータを処理しきれず、バッファが溢れシステムエラーになったものと推測できます。
さいごに
同期書き込みより圧倒的に速度が出ますし、大量データを読み込みながら書き込むケースでもメモリ効率が良いので是非試して頂ければと思います!
参考
- Node.js Stream を使ってみる
- Streamの詳細解説があり、理解に役立ちました!